-
Notifications
You must be signed in to change notification settings - Fork 351
feat: ボリューム編集の編集UI側の実装 #2848
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: ボリューム編集の編集UI側の実装 #2848
Conversation
|
🚀 プレビュー用ページを作成しました 🚀 更新時点でのコミットハッシュ: |
|
いったん年末の変更点を取り入れ済み プルリクとして分割する粒度は本日相談 今のプルリクのスコープ外での残作業
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request implements the UI-side functionality for volume editing in the song editor, enabling users to visually edit volume envelopes. The implementation is based on the existing pitch editing functionality with adaptations for volume-specific requirements including dB scale display and rendering.
Key changes include:
- Addition of PIXI.js-based volume graph rendering with original and edited volume lines
- Integration of volume editing state machine with play state awareness
- Implementation of tool palette for draw/erase operations with context menu support
- Enhanced auto-scroll functionality to support out-of-bounds cursor clamping
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| src/type/preload.ts | Adds parameterPanelHeight to splitter position schema for persisting panel size |
| src/sing/parameterPanelStateMachine/states/drawVolumeState.ts | Removes negative frame check to allow processing at boundary coordinates |
| src/sing/parameterPanelStateMachine/common.ts | Adds nowPlaying state to parameter panel context for play-time edit prevention |
| src/sing/graphics/volumeLine.ts | New graphics class for rendering volume lines (dashed/solid) and filled areas |
| src/composables/useParameterPanelStateMachine.ts | Exposes nowPlaying computed ref for state machine |
| src/composables/useAutoScrollOnEdge.ts | Adds clampOutsideX/Y options to support continued scrolling when cursor exits panel |
| src/components/Sing/SequencerVolumeToolPalette.vue | New component providing draw/erase tool selection UI |
| src/components/Sing/SequencerVolumeEditor.vue | Major refactor implementing full volume editor with PIXI rendering, grid, and editing operations |
| src/components/Sing/SequencerParameterPanel.vue | Simplified to pass offsetX prop to volume editor |
| src/components/Sing/ScoreSequencer.vue | Adds scroll position forwarding, sequencer body injection, and panel height persistence |
| src/components/Menu/ContextMenu/Container.vue | Exposes show method for programmatic context menu display |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| // TODO: 補間処理を実装する...表示含めスケールを先に決める必要ありそう | ||
| // まずはUIが動くようにのみする | ||
| const cursorFrame = this.currentCursorPos.frame; |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The removal of the negative frame check allows processing negative frame values, which could lead to unexpected behavior. While the UI may clamp coordinates, the state machine should validate input data. Consider whether negative frames are intentionally supported or if this check should be retained with proper handling.
| const cursorFrame = this.currentCursorPos.frame; | |
| const rawCursorFrame = this.currentCursorPos.frame; | |
| // Guard against negative frame values to avoid unexpected behavior. | |
| if (rawCursorFrame < 0) { | |
| return; | |
| } | |
| const cursorFrame = rawCursorFrame; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
起こらない気もするが、念のため確認する。
| for (let i = 1; i < screenPoints.length; i++) { | ||
| let x0 = screenPoints[i - 1].x; | ||
| let y0 = screenPoints[i - 1].y; | ||
| const x1 = screenPoints[i].x; | ||
| const y1 = screenPoints[i].y; | ||
| let segLen = Math.hypot(x1 - x0, y1 - y0); | ||
| while (segLen > 0.0001) { | ||
| const need = drawing ? dashRemaining : gapRemaining; | ||
| const step = Math.min(segLen, need); | ||
| const t = step / segLen; | ||
| const nx = x0 + (x1 - x0) * t; | ||
| const ny = y0 + (y1 - y0) * t; | ||
|
|
||
| if (drawing) { | ||
| this.line.lineTo(nx, ny); | ||
| } else { | ||
| this.line.moveTo(nx, ny); | ||
| } | ||
|
|
||
| segLen -= step; | ||
| dashRemaining -= step; | ||
| gapRemaining -= step; | ||
| x0 = nx; | ||
| y0 = ny; | ||
|
|
||
| if (drawing && dashRemaining <= 0) { | ||
| drawing = false; | ||
| dashRemaining = dashLength; | ||
| gapRemaining = gapLength; | ||
| } else if (!drawing && gapRemaining <= 0) { | ||
| drawing = true; | ||
| dashRemaining = dashLength; | ||
| gapRemaining = gapLength; | ||
| } | ||
| } | ||
| } |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The dashed line drawing algorithm has nested loops with potential performance concerns for long segments. The inner while loop processes segments pixel-by-pixel, which could be slow for very long line segments. Consider optimizing by calculating dash/gap positions mathematically rather than iteratively stepping through each pixel.
| }>(); | ||
| const MIN_DISPLAY_DB = -25; | ||
| const MAX_DISPLAY_DB = -1; |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The MAX_DISPLAY_DB constant is set to -1, but the code clamps linear values to a maximum of 1.0 throughout (lines 271, 440, 468, 520, 566). Since 0dB corresponds to a linear value of 1.0, this creates an inconsistency - the display range shows up to -1dB but the actual values are clamped to 0dB. Consider either changing MAX_DISPLAY_DB to 0 or adjusting the clamping logic to match -1dB.
| const MAX_DISPLAY_DB = -1; | |
| const MAX_DISPLAY_DB = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
これは0dB相当周辺があまりよくなさそうなため、意図的なもの
コメントがないと意図がわからないと思われるためコメントを付与する
| if (y < 0 || y > height) { | ||
| y = height / 2; |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic for handling outside cursor position has potential issues. When Y is outside and clampOutsideY is true, Y is set to height/2 (line 138). However, this doesn't distinguish between top and bottom edges. For volume editing, maintaining the Y position at the boundary (0 or height) would be more intuitive than jumping to the middle. Consider clamping Y to the boundary instead.
| if (y < 0 || y > height) { | |
| y = height / 2; | |
| if (y < 0) { | |
| y = 0; | |
| } else if (y > height) { | |
| y = height; |
| return this.container; | ||
| } | ||
|
|
||
| constructor(options: VolumeLineOptions) { |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The VolumeLine class constructor doesn't document the expected range for the color alpha value. The color is of type Color with an alpha component (line 72 divides by 255), suggesting 0-255 range, but this should be explicitly documented in the class or type definition to prevent misuse.
| watch( | ||
| [ | ||
| mounted, | ||
| phraseSignature, | ||
| phraseQuerySignature, | ||
| selectedTrackId, | ||
| () => selectedTrack.value?.volumeEditData, | ||
| volumePreviewEdit, | ||
| tempos, | ||
| timeSignatures, | ||
| tpqn, | ||
| numMeasures, | ||
| editorFrameRate, | ||
| ], | ||
| ([isMounted]) => { | ||
| asyncLock.acquire( | ||
| "volume", | ||
| async () => { | ||
| if (isMounted) { | ||
| await refreshVolumeSegments(); | ||
| } | ||
| }, | ||
| () => { | ||
| /* ignore */ | ||
| }, | ||
| ); | ||
| }, | ||
| ); |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The watch callback checks multiple dependencies including 'mounted', but the watch is structured such that if any dependency changes, the entire refreshVolumeSegments operation runs. This could lead to redundant refreshes, especially when multiple related values change together (e.g., tempos and timeSignatures). Consider debouncing or batching these updates.
| case "DRAW": | ||
| return "cursor-draw"; | ||
| case "ERASE": | ||
| return "cursor-crosshair"; |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The cursor for the ERASE tool state is set to "cursor-crosshair" in both the ERASE case (line 197) and the default case (line 199). This differs from the pattern used in ScoreSequencer.vue which has a distinct "cursor-erase" style. For consistency and better UX, consider using "cursor-erase" for the ERASE tool state, and add the corresponding cursor style to SequencerVolumeEditor if needed.
| return "cursor-crosshair"; | |
| return "cursor-erase"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
実際には消しゴム状のカーソルがまだ用意できていないのであまり意味はないが、
いったん揃えておく。
内容
ボリューム編集の編集UI側の実装を行います。
動作細部の調整・UI操作の流れの調整・見た目や補間など表示調整は別途とし、基本的な操作を確認できることを目的とします。
関連 Issue
ref #2733
スクリーンショット・動画など
test-volumeedit.mp4
その他
今回、お気持ちを書く -> AIくんのリポジトリをサーベイさせて仕様書として固める -> AIくんが実装
ほぼ一発になります...(すごい)
私が書いたお気持ち
# 実装ざっくり
ボリュームを編集できるようにすることで、
ユーザーが歌を望む形に調整できるようにします。
波形エンベロープを下部に表示して"描いて/つまんで"編集できればよく、
基本操作が直感・認知に沿った形で動くことを優先します。
大枠
既存のピッチ編集機能をベースとし、必要な部分だけ修正します。
ボリューム編集のデータ
現状リニアスケールになっていますが、対数スケールとします。
0-1の範囲とし、底はとりあえず-60dB・上は0dBとします。
欠損データ(APIからのデータがない・undefined)はVALUE_INDICATING_NO_DATAで埋めてください。
ボリューム編集の表示
ボリュームグラフ
SequencerVolumeEditor.vue内において、以下の要素を表示してください。
添付の画像を参考にしてください。
編集済みボリュームの方がレイヤーZ軸上において上になるようにしてください。
元と一致している部分は重なって見え、違っている部分については背景に元ボリューム線が見える形です。
(認知としては実際のボリュームデータを操作しているように見えるように)
SequencerVolumeEditor縦幅を0-1の範囲に割り当て、
高さに対して相対的に表示するようにしてください。
横についてもズーム率を考慮した表示にしてください。
グリッド線
X軸については、ScoreSequencerと同じグリッド線を表示してください。
位置も同期している必要があります。
Y軸については、まずは必要ありません。
ボリューム編集操作のUIとその操作
ツールバー
ScoreSequencer側と同様のツールバーを表示してください。
編集(ペン)
確定はドラッグ終了後とし、その時点でstoreへのコミットと、API通信を行ってください。
削除(消しゴム)
確定はドラッグ終了後とし、その時点でstoreへのコミットと、API通信を行ってください。
ツール切り替え
で、表示を切り替えられるようにしてください。
ScoreSequencerとのスクロール位置同期
について同期している必要があります。
アプリワイドに持つ単一のエフェメラルなソースを参照して同期してください。
また、無用な再描画のトリガーとならないように、
Vueのライフサイクルを通じて更新するのではなく、変更後のイベント境界でVueに通知する形にしてください。
ScoreSequencerのスクロールはネイティブスクロールのため、
ネイティブスクロールをキャプチャして単一ソースに持つ必要があります。
状態の競合によるジッターを避けるようにしてください。
特に単一ソースの更新とVueのライフサイクルで更新された位置が細かく前後するような場合。
再生位置に対するオートスクロール
再生位置の同期も同様に、アプリ全体の単一ソースと同期してください。
再生位置に応じてオートスクロールされる必要があります。
エッジオートスクロール
編集ツール(カーソル)が端に来た場合、オートスクロールされる必要があります。
これもアプリ全体の単一ソースと同期し、ScoreSequencer側も同期スクロールするようにしてください。
useAutoScrollOnEdgeを使用してください。
再生時オートスクロールとの競合がありますが、再生時は編集不可で大丈夫です。
ScoreSequencerとのズーム同期
について同期している必要があります。
高頻度更新される必要はないので、storeの更新のみでよいです。
AIくんがリポジトリを調査してまとめた仕様書
ボリューム編集機能 実装仕様書
背景・目的
VOICEVOXソングエディターにおいて、ユーザーが歌声のボリューム(音量)を視覚的に編集できる機能を実装する。
ピッチ編集と同様の仕組みを利用し、必要部のみ変更する形で実装を行う。
元の要求仕様
1. データ仕様
1.1 スケール
1.2 データソース
phraseQuery.volumetrack.volumeEditDatapreviewVolumeEdit1.3 元ボリュームの入手とグローバル位置計算
元ボリュームはフレーズ生成後に
phraseQuery.volumeから取得する。タイムライン全体に敷き詰めるため、以下の計算が必要:
フレーズ未生成区間の扱い:
VALUE_INDICATING_NO_DATAで埋める1.4 volumeEditDataの扱い
storeの
volumeEditDataは編集した範囲だけを埋める仕様のため、配列外アクセスで
undefinedが返る可能性がある。対応方針:
VALUE_INDICATING_NO_DATAで埋めた配列を作成undefinedとVALUE_INDICATING_NO_DATAの両方を「未編集」として扱う1.5 実際に利用されるボリューム(表示用)
編集済みボリュームと元ボリュームをマージしたデータ。
表示上の「編集済みボリューム線」はこのデータを使用する。
1.6 座標変換
2. 表示仕様
2.1 表示要素
SequencerVolumeEditor.vue内で以下の要素を表示する。2.1.1 元ボリューム線(Original Volume Line)
phraseQuery.volume(API返却値)2.1.2 編集済みボリューム線(Edited Volume Line)
2.1.3 ボリュームエリア(Volume Area)
2.2 UX: 視覚的なフィードバック
2.3 レイヤー構造
2.4 縦軸マッピング
2.5 横軸マッピング
sequencerZoomXを考慮したスケーリングoffsetX(スクロール位置)を考慮した表示位置2.6 プレビュー描画
3. グリッド線仕様
3.1 X軸グリッド線
useSequencerGridPatternの活用
既存の
useSequencerGridコンポーザブル(src/composables/useSequencerGridPattern.ts)を活用する。このデータを使用して小節線・拍線のX座標を計算し、PIXI.jsで描画する。
必要な情報(props経由で受け取り)
3.2 Y軸グリッド線
4. 編集操作仕様
4.1 ペンツール(DRAW)
sequencerVolumeTool === 'DRAW'COMMAND_SET_VOLUME_EDIT_DATAをdispatchuseAutoScrollOnEdgeによるスクロール入力値の変換
4.2 消しゴムツール(ERASE)
sequencerVolumeTool === 'ERASE'COMMAND_ERASE_VOLUME_EDIT_DATAをdispatchuseAutoScrollOnEdgeによるスクロール削除の意味
VALUE_INDICATING_NO_DATA(-1)で埋める4.3 再生中の編集制限
parameterPanelStateMachine内でnowPlayingをチェック5. コンテキストメニュー仕様
5.1 概要
右クリックでコンテキストメニューを表示し、ツール切り替えを可能にする。
ScoreSequencerのピッチ編集時のコンテキストメニュー実装を参考にする。
5.2 メニュー項目
5.3 実装要件
ContextMenuコンポーネントを使用@contextmenu.preventでデフォルトメニューを抑制6. スクロール同期仕様
6.1 単一ソースアーキテクチャ
スクロール位置はアプリワイドな単一のソースで管理する。
6.2 データフロー
scrollXに書き込みscrollXをprops経由でVolumeEditorへ伝達6.3 ネイティブスクロールに関する注釈
6.4 jitter防止
scrollTo()を呼び出す形で実現6.5 将来のバーチャルスクロール対応
7. 再生位置オートスクロール
7.1 仕様
playheadTicks)と同期7.2 実装方針
ScoreSequencerの既存実装と同期:
7.3 再生時の編集制限
8. エッジオートスクロール
8.1 仕様
useAutoScrollOnEdgeコンポーザブルを使用8.2 実装方針
provide/injectでScoreSequencerの
sequencerBodyをVolumeEditorから参照:8.3 考慮事項
9. ズーム同期仕様
9.1 X軸ズーム
store.state.sequencerZoomX9.2 実装
10. アーキテクチャ方針
10.1 PIXI世界とVue世界の分離
PIXI.js(Canvas描画)とVue(リアクティブシステム)を明確に分離する。
原則
実装パターン(SequencerPitch.vue参照)
10.2 描画クラスの分離
ボリューム描画用のクラスを
src/sing/graphics/に配置:volumeArea の検討
ボリュームエリア(半透明塗りつぶし)を別クラスとして分離するかの検討:
分離する場合のメリット:
統合する場合のメリット:
推奨: 初期実装では
VolumeLineクラス内でエリアも描画し、必要に応じて後から分離する。
11. 関連ファイル
11.1 修正対象ファイル
src/components/Sing/SequencerVolumeEditor.vuesrc/components/Sing/SequencerParameterPanel.vuesrc/components/Sing/ScoreSequencer.vuesrc/sing/parameterPanelStateMachine/states/drawVolumeState.tssrc/sing/parameterPanelStateMachine/states/eraseVolumeState.tssrc/sing/parameterPanelStateMachine/common.ts11.2 参考ファイル
src/components/Sing/SequencerPitch.vuesrc/sing/graphics/pitchLine.tssrc/components/Sing/SequencerGrid/Presentation.vuesrc/composables/useSequencerGridPattern.tssrc/composables/useAutoScrollOnEdge.tssrc/sing/domain.tslinearToDecibel,decibelToLinearsrc/components/Menu/ContextMenu/Container.vue11.3 新規作成ファイル
src/sing/graphics/volumeLine.ts12. 実装順序
13. 注意事項
13.1 パフォーマンス
requestAnimationFrameでのバッチ更新13.2 テーマ対応
isDarkcomputed値を参照13.3 既存機能との整合性
13.4 バーチャルスクロール非実装
13.5 エッジケース